package luck.technology.booktext;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.SubscriptSpan;
import android.text.style.SuperscriptSpan;
import android.text.style.UnderlineSpan;
import android.util.Log;
import android.util.TypedValue;

public class BookText implements CharSequence, Appendable {
	
	/** Constructors **/
	
	public BookText () {}
	public BookText (String textStr) {
		append(textStr);
	}
	
	/** Constants **/
	
	public static final float
			STYLE_NORMAL = 1f,  
			STYLE_TITLE = 2f,
			STYLE_SUBTITLE = 1.5f,
			STYLE_SMALL_TEXT = 0.8f;
	static final float UNDEFINED = -1;
	private static final float INTERVAL = 1.5f;
	static final int UNDEFINED_COLOR = 0x00ffffff;
	static final Typeface DEF_TYPEFACE = Typeface.SANS_SERIF;
	
	private static class Names {
		static final String
				JUSTIFY = "justify",
				STYLE_FACTOR = "factor",
				TEXT = "text",
				TEXT_STRING = "str",
				TAG = "tag",
				BOLD_ITALIC = "b_i",
				BOLD = "b",
				ITALIC = "i",
				SUP = "sup",
				SUB  = "sub",
				STRIKE = "s",
				UNDERLINE = "u",
				MONO = "mono";
	}
	
	/** Subclasses **/
	
	private static class Piece {
		
		String textStr, tag;
		
		boolean	boldItalic = false,
				bold = false,
				italic = false, 
				sup = false,
				sub = false,
				strike = false,
				underline = false,
				mono = false;
	}
	
	private static class ProcessedPiece {
		
		static final int 
				WORD = 0,
				SPACE = 1,
				NEW_LINE = 2;
		
		int type = WORD;
		
		float 	yOffset = 0, 
				xLeft = UNDEFINED,
				xRight = UNDEFINED,
				yTop = UNDEFINED,
				yBottom = UNDEFINED;
		
		String textStr, tag;
		
		Paint paint;
		
		ProcessedPiece copy(){
			
			ProcessedPiece result = new ProcessedPiece();
			result.type = type;
			result.xLeft = xLeft;
			result.xRight = xRight;
			result.yTop = yTop;
			result.yBottom = yBottom;
			result.yOffset = yOffset;
			result.paint = paint;
			result.textStr = new String(textStr); 
			if (tag != null) result.tag = new String(tag);
			return result;
		}
	}
	
	private static class TextLine {
		
		boolean justify = false;
		float width = 0;
		List <ProcessedPiece> text = new ArrayList <ProcessedPiece> (); 
	}
	
	/** Member vars **/
	
	private boolean mJustify = true;	
	
	private float 
			mTextSize = UNDEFINED, 
			mStyleFactor = STYLE_NORMAL,
			mRawTextSize = UNDEFINED,
			mYStep = 0;
	
	private int mTextColor = UNDEFINED_COLOR;
	
	private Typeface mTypeface = null;
	
	private List <Piece> mInputText = new ArrayList <Piece> ();
	private List <ProcessedPiece> mProcessedText;
	List <TextLine> mLines;
	
	private CharSequence mCharSequence = null;
	
	private boolean mDecode = false;
	
	/** Methods **/
	
	void setTypeface(Typeface typeface) {
		
		this.mTypeface = typeface;
	}
	
	Typeface getTypeface() {
		
		if (mTypeface == null) return DEF_TYPEFACE;
		return mTypeface;
	}
	
	String getTag(float x, float y){
		
		Log.i(this.getClass().getSimpleName(), "x = " + x + "; y = " + y);
		for(TextLine textLine:mLines){
			for(ProcessedPiece p:textLine.text){
				if (x >= p.xLeft
						&& x <= p.xRight
						&& y >= p.yTop
						&& y <= p.yBottom){
					Log.i(this.getClass().getSimpleName(), "text = " + p.textStr);
					return p.tag;
				}
			}
		}
		
		return null;
	}
	
	void setTextSize(float textSize){
		
		this.mRawTextSize = textSize;
	}
	
	float getTextSize(){
		
		return mRawTextSize;
	}
	
	public void setStyleFactor(float styleFactor) {
		
		mStyleFactor = styleFactor;
	}
	
	public float getStyleFactor() {
		
		return mStyleFactor;
	}
	
	public void setDecodeHTMLSymbols(boolean enabled) {
		
		mDecode = enabled;
	}
	
	public boolean isDecodeHTMLSymbolsEnabled() {
		
		return mDecode;
	}
	
	private String decode (String str) {
		
		if (!mDecode) return str;
		String pre = str.startsWith(" ") ? " " : "";
		String post = str.endsWith(" ") ? " " : "";
		return pre + Html.fromHtml(str.trim()) + post;
	}
	
	public void clear () {
		
		synchronized (mInputText) {
			mInputText.clear();
			mCharSequence = null;
		}
	}
	
	public BookText append (
			String textStr, 
			boolean boldItalic, 
			boolean bold, 
			boolean italic, 
			boolean sup, 
			boolean sub, 
			boolean strike, 
			boolean underline, 
			boolean mono,
			String tag){
		
		Piece piece = new Piece();
		piece.textStr = textStr;
		piece.boldItalic = boldItalic;
		piece.bold = bold;
		piece.italic = italic;
		piece.sup = sup;
		piece.sub = sub;
		piece.strike = strike;
		piece.underline = underline;
		piece.mono = mono;
		piece.tag = tag;
		
		synchronized (mInputText) {
			mInputText.add(piece);
			mCharSequence = null;
		}
		return this;
	}
	
	public BookText append (
			String textStr, 
			int boldItalic, 
			int bold, 
			int italic, 
			int sup, 
			int sub, 
			int strike, 
			int underline,
			int mono,
			String tag){
		
		append(textStr, boldItalic > 0, bold > 0, italic > 0, sup > 0, sub > 0,
				strike > 0, underline > 0, mono > 0, tag);
		
		return this;
	}
	
	public BookText append (String textStr){
		
		this.append(textStr, 0, 0, 0, 0, 0, 0, 0, 0, null);
		return this;
	}

	public BookText appendItalic (String textStr){
		
		this.append(textStr, 0, 0, 1, 0, 0, 0, 0, 0, null);
		return this;
	}

	public BookText appendBold (String textStr){
		
		this.append(textStr, 0, 1, 0, 0, 0, 0, 0, 0, null);
		return this;
	}

	public BookText appendBoldItalic (String textStr){
		
		this.append(textStr, 1, 0, 0, 0, 0, 0, 0, 0, null);
		return this;
	}
	
	public BookText appendLink (String textStr, String link){
		
		this.append(textStr, 0, 0, 0, 0, 0, 0, 1, 0, link);
		return this;
	}

	public BookText appendStriked (String textStr){
		
		this.append(textStr, 0, 0, 0, 0, 0, 1, 0, 0, null);
		return this;
	}
	
	public BookText appendSuperscript (String textStr){
		
		this.append(textStr, 0, 0, 0, 1, 0, 0, 0, 0, null);
		return this;
	}
	
	public BookText appendSubscript (String textStr){
		
		this.append(textStr, 0, 0, 0, 0, 1, 0, 0, 0, null);
		return this;
	}
	
	public BookText appendCode (String textStr){
		
		this.append(textStr, 0, 0, 0, 0, 0, 0, 0, 1, null);
		return this;
	}
	
	void process (Context context) {
		
		if (mRawTextSize == UNDEFINED || mTextColor == UNDEFINED_COLOR) {
			int[] textAttr = new int[] { android.R.attr.textSize, android.R.attr.textColor };
			int indexOfAttrTextSize = 0;
			int indexOfAttrTextColor = 1;

			TypedValue typedValue = new TypedValue();
			context.getTheme().resolveAttribute(android.R.attr.textAppearanceMedium, typedValue, true);
			TypedArray a = context.obtainStyledAttributes(typedValue.data, textAttr);
			if (mRawTextSize == UNDEFINED) mRawTextSize = a.getDimensionPixelSize(indexOfAttrTextSize, -1);
			if (mTextColor == UNDEFINED_COLOR) mTextColor = a.getColor(indexOfAttrTextColor, Color.TRANSPARENT);
			a.recycle();
		}
		
		mTextSize = mRawTextSize * mStyleFactor;
		mProcessedText = new ArrayList <ProcessedPiece> ();
		
		for (Piece piece:mInputText) {
			Paint paint = new Paint();
			
			Typeface typeface;
			if (piece.mono) {
				typeface = Typeface.MONOSPACE;
			} else {
				if (mTypeface == null) 
					typeface = DEF_TYPEFACE;
				else typeface = mTypeface;
			}
			
			/** Process text with style attrs **/
			
			paint.setTextAlign(Align.LEFT);
			paint.setAntiAlias(true);
			paint.setColor(mTextColor);
			if (piece.sup || piece.sub) paint.setTextSize( mTextSize / 2);
			else paint.setTextSize(mTextSize);
			if (piece.italic || piece.boldItalic) paint.setTextSkewX(-0.25f);
			if (piece.bold || piece.boldItalic) 
				paint.setTypeface(Typeface.create(typeface, Typeface.BOLD)); 
			else paint.setTypeface(typeface);
			if (piece.strike) paint.setFlags(Paint.STRIKE_THRU_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
			if (piece.underline) paint.setFlags(Paint.UNDERLINE_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
			
			/** Slice text to the pieces **/
			
			ArrayList <Integer> points = new ArrayList <Integer> ();
			String str = decode(piece.textStr).replace("\r\n", "\n").replace("\r", "\n");
			int pos = 0;
			points.add(Integer.valueOf(0));
			while (pos != -1){
				int pos_sp = str.indexOf(" ", pos);
				int pos_br = str.indexOf("\n", pos);
				if (pos_sp != -1 && pos_br != -1) {
					pos = Math.min(pos_sp, pos_br);
				}
				else if (pos_sp == -1 && pos_br != -1) {
					pos = pos_br;
				}
				else if (pos_br == -1 && pos_sp != -1) {
					pos = pos_sp;
				} 
				else {
					pos = -1;
				}
				if(pos != -1) {
					points.add(Integer.valueOf(pos));
					points.add(Integer.valueOf(pos+1));
					pos++;
				}
			}
			points.add(Integer.valueOf(str.length()));
			for(int i = 0; i < points.size() - 1; i ++){
				if (points.get(i).intValue() != points.get(i + 1).intValue()) {
					
					String word = str.substring(points.get(i), points.get(i + 1));
					
					ProcessedPiece p = new ProcessedPiece();
					p.paint = paint;
					mProcessedText.add(p);
					
					if (word.equals(" ")) {
						p.type = ProcessedPiece.SPACE;
						p.tag = piece.tag;
						p.textStr = word;						
					} else if (word.equals("\n")) {
						p.type = ProcessedPiece.NEW_LINE;					
					} else {
						if (piece.sup) p.yOffset = - paint.getTextSize();
						p.type = ProcessedPiece.WORD;
						p.textStr = word;
						p.tag = piece.tag;
					}
				}
			}
		}
	}
	
	int format (int width) {
		
		/** Format the text **/
		
		if (mProcessedText == null) return 0;
		mLines = new ArrayList <TextLine> ();
		mYStep = mTextSize * INTERVAL;
		int line = 0;
		float x = 0f;
		TextLine currentLine = new TextLine();
		mLines.add(currentLine);
		for(ProcessedPiece p:mProcessedText){
			if(p.type == ProcessedPiece.NEW_LINE){
				x = 0;
				line ++;
				currentLine.justify = false;
				currentLine = new TextLine();
				mLines.add(currentLine);
			} else {
				if (x + p.paint.measureText(p.textStr) > width && x > 0) {
					x = 0;
					line ++;
					currentLine.justify = true;
					currentLine = new TextLine();
					mLines.add(currentLine);
				}
				p.xLeft = x;
				if (x > 0 || p.type != ProcessedPiece.SPACE)
					x += p.paint.measureText(p.textStr);
				p.xRight = x;
				p.yTop = mYStep * ((float) line) + p.yOffset;
				p.yBottom = p.yTop + mYStep - p.yOffset;
				currentLine.text.add(p);
				currentLine.width = p.xRight;
				
				/** If the word is too long for screen width **/
				
				if (p.xLeft == 0f && p.xRight > width){
					
					/** Split the word BEGIN **/
					
					String word = new String(p.textStr);
					String tag = p.tag;
					float yOffset = p.yOffset;
					Paint paint = p.paint;
					
					ProcessedPiece p0 = p.copy();
					currentLine.text.remove(p);
					currentLine.text.add(p0);
					
					int count = paint.breakText(word, true, width, null);
					if (count == 0) count = 1;
					p0.textStr = word.substring(0, count);
					p0.xRight = p0.paint.measureText(p0.textStr);
					currentLine.width = p0.xRight;
					int offset = count;
					
					while (word.substring(offset).length() > 0) {
						
						count = paint.breakText(word.substring(offset), true, width, null);
						if (count == 0) count = 1;
						x = 0;
						line ++;
						currentLine.justify = false;
						currentLine = new TextLine();
						mLines.add(currentLine);
						ProcessedPiece p1 = new ProcessedPiece();
						p1.paint = paint;
						p1.tag = tag;
						p1.yOffset = yOffset;
						p1.textStr = word.substring(offset, offset + count);
						p1.xLeft = x;
						x += p1.paint.measureText(p1.textStr);
						p1.xRight = x;
						p1.yTop = mYStep * ((float) line) + p1.yOffset;
						p1.yBottom = p1.yTop + mYStep - p1.yOffset;
						currentLine.text.add(p1);
						offset += count;
					}
					
					/** Split the word END **/
					
				}
			}
		}
		
		int result = (int) (mLines.size() * mYStep);
		
		if (!mJustify) return result;
		
		/** Justify the text **/
		
		for (TextLine textLine:mLines){
			if(textLine.justify){
				textLine.width = width;

				/** Remove spaces from the start of the line  **/
					
				ProcessedPiece p; 
				while (textLine.text.size() > 0 
						&& (p = textLine.text.get(0)).type == ProcessedPiece.SPACE) {
					textLine.text.remove(p);
				}
					
				/** Remove spaces from the end of the line **/
				while (textLine.text.size() > 0
						&& (p = textLine.text.get(textLine.text.size() - 1))
								.type == ProcessedPiece.SPACE) {
					textLine.text.remove(p);
				}
				
				/** Count the space width **/
				
				float rawTextWidth = 0;
				int spacesCount = 0;
				for (ProcessedPiece p0: textLine.text){
					if(p0.type == ProcessedPiece.SPACE){
						spacesCount++;
					} 
					else if(p0.type == ProcessedPiece.WORD){
						rawTextWidth += p0.paint.measureText(p0.textStr);
					}
				}
				
				/** Reformat the line of text **/
				
				if(rawTextWidth > 0 && spacesCount > 0){
					float spaceSize = width;
					spaceSize -= rawTextWidth;
					spaceSize /= spacesCount;
					if (spaceSize > 0) {
						x = 0;
						for (ProcessedPiece p0: textLine.text){
							p0.xLeft = x;
							if(p0.type == ProcessedPiece.SPACE){
								x += spaceSize;
							} else {
								x += p0.paint.measureText(p0.textStr);
							}
							p0.xRight = x;
						}
					}
				}
			}
		}
		return result;
	}
	
	void setTextColor(int color){
		
		mTextColor = color; 
	}
	
	int getTextColor() {
		
		return mTextColor; 
	}
	
	void setJustify(boolean justify) {
		
		mJustify = justify;
	}
	
	boolean getJustify() {
		
		return mJustify;
	}
	
	void draw (Canvas c, float x, float y) {
		
		for (TextLine textLine:mLines) {
			for (ProcessedPiece p:textLine.text) {
				c.drawText(
						p.textStr, 
						p.xLeft + x, 
						p.yTop + y + mTextSize, 
						p.paint);
			}
		}
	}
	
	int getLineBounds (int line, Rect bounds){

		bounds.setEmpty();
		if (mLines == null) return 0;
		try {
			
			TextLine textLine = mLines.get(line); 
			
			bounds.top = (int)(line * mYStep);
			bounds.bottom = (int)((line + 1) * mYStep);
			bounds.left = 0;
			bounds.right = (int)textLine.width;
			return (int)(line * mYStep + mTextSize);
			
		} catch(IndexOutOfBoundsException e) {
			
			return 0;
		}
	}
	
	public JSONObject exportJSON(){
		
		JSONObject result = new JSONObject();
		try {
			result.put(Names.STYLE_FACTOR, mStyleFactor);
			result.put(Names.JUSTIFY, mJustify);
			JSONArray array = new JSONArray();
			for (Piece p:mInputText){
				JSONObject obj = new JSONObject();
				obj.put(Names.TEXT_STRING, p.textStr);
				obj.put(Names.TAG, p.tag);
				obj.put(Names.BOLD_ITALIC, p.boldItalic ? 1 : 0);
				obj.put(Names.BOLD, p.bold ? 1 : 0);
				obj.put(Names.ITALIC, p.italic ? 1 : 0);
				obj.put(Names.STRIKE, p.strike ? 1 : 0);
				obj.put(Names.UNDERLINE, p.underline ? 1 : 0);
				obj.put(Names.SUP, p.sup ? 1 : 0);
				obj.put(Names.SUB, p.sub ? 1 : 0);
				obj.put(Names.MONO, p.mono ? 1 : 0);
				array.put(obj);
			}
			result.put(Names.TEXT, array);
			
		} catch (JSONException e) {
			e.printStackTrace();
		}
		return result;
	}
	
	public JSONException importJSON(JSONObject inObj){
		
		List <Piece> text = new ArrayList <Piece> ();
		try {
			float factor = Double.valueOf(inObj.getDouble(Names.STYLE_FACTOR)).floatValue();
			JSONArray array = inObj.getJSONArray(Names.TEXT);
			boolean justify = inObj.getBoolean(Names.JUSTIFY);
			
			for (int i = 0; i < array.length(); i++) {
				JSONObject obj = array.getJSONObject(i);
				Piece p = new Piece();
				p.textStr = obj.getString(Names.TEXT_STRING);
				p.boldItalic = obj.getInt(Names.BOLD_ITALIC) > 0;
				p.bold = obj.getInt(Names.BOLD) > 0;
				p.italic = obj.getInt(Names.ITALIC) > 0;
				p.strike = obj.getInt(Names.STRIKE) > 0;
				p.underline = obj.getInt(Names.UNDERLINE) > 0;
				p.sup = obj.getInt(Names.SUP) > 0;
				p.sub = obj.getInt(Names.SUB) > 0;
				try {
					p.tag = obj.getString(Names.TAG);
				} catch (JSONException e) {
					p.tag = null;
				}
				try {
					p.mono = obj.getInt(Names.MONO) > 0;
				} catch (JSONException e) {
					p.mono = false;
				}
				text.add(p);
			}
			
			mInputText = text;
			mStyleFactor = factor;
			mJustify = justify;
			mCharSequence = null;
		} catch (JSONException e) {
			e.printStackTrace();
			return e;
		}
		
		return null;
	}
	
	public Spanned getSpText(){
		
		SpannableStringBuilder ss = new SpannableStringBuilder();
		
		for (Piece piece:mInputText){
			
			SpannableString ss0 = new SpannableString(decode(piece.textStr));
			ss0.setSpan(new RelativeSizeSpan(mStyleFactor), 0, ss0.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
			if (piece.italic)
				ss0.setSpan(new StyleSpan(Typeface.ITALIC), 0, ss0.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
			if (piece.bold)
				ss0.setSpan(new StyleSpan(Typeface.BOLD), 0, ss0.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
			if (piece.boldItalic)
				ss0.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, ss0.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
			if (piece.sup)
				ss0.setSpan(new SuperscriptSpan(), 0, ss0.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);			
			if (piece.sub)
				ss0.setSpan(new SubscriptSpan(), 0, ss0.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
			if (piece.underline)
				ss0.setSpan(new UnderlineSpan(), 0, ss0.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
			if (piece.strike)
				ss0.setSpan(new StrikethroughSpan(), 0, ss0.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

			ss.append(ss0);
		}
		return ss;
	}
	
	private CharSequence toCharSequence() {
		
		if (mCharSequence == null) mCharSequence = getSpText();
		
		return mCharSequence;
	}
	
	/** Implementation of CharSequence **/
	
	@Override
	public String toString() {
		
		return toCharSequence().toString();
	}
	@Override
	public char charAt(int index) {
		
		return toCharSequence().charAt(index);
	}
	@Override
	public int length() {
		
		return toCharSequence().length();
	}
	@Override
	public CharSequence subSequence(int start, int end) {
		
		return toCharSequence().subSequence(start, end);
	}

	/** Implementation of Appendable **/
	
	@Override
	public Appendable append(char arg0) throws IOException {
		
		append(String.valueOf(arg0));
		return this;
	}
	@Override
	public Appendable append(CharSequence arg0) throws IOException {
		
		append(arg0.toString());
		return this;
	}
	@Override
	public Appendable append(CharSequence arg0, int arg1, int arg2)
			throws IOException {
		
		append(arg0.subSequence(arg1, arg2).toString());
		return this;
	}
}
